package com.katesoft.scale4j.persistent.spring;

import static com.katesoft.scale4j.persistent.spring.HibernateSessionUtility.hibernateProperties;
import static com.katesoft.scale4j.persistent.spring.HibernateSessionUtility.performSchemaUpdate;
import static com.katesoft.scale4j.persistent.spring.HibernateSessionUtility.storeSchemaCreateDeleteScripts;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.Lock;

import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceException;

import org.hibernate.HibernateException;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.ImprovedNamingStrategy;
import org.hibernate.ejb.Ejb3Configuration;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;

import com.katesoft.scale4j.common.lock.IDistributedLockProvider;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.hibernate.EventListeners;
import com.katesoft.scale4j.persistent.hibernate.ICommonHibernateConfiguration;
import com.katesoft.scale4j.persistent.hibernate.StoreUpdateScriptsConfiguration;

/**
 * @author kate2007
 */
@SuppressWarnings("rawtypes")
public class LocalAnnotationEntityManagerFactory extends LocalContainerEntityManagerFactoryBean
         implements ICommonHibernateConfiguration, ILocalHibernateSessionBuilder {
   private static final ThreadLocal<Object> configTimeRegionFactoryHolder = new ThreadLocal<Object>();
   private final Logger log = LogFactory.getLogger(getClass());
   private final List<Class> annotatedClasses = new LinkedList<Class>();
   private final List configLocations = new LinkedList();
   private List resources = new LinkedList();
   private DatabasePopulator databasePopulator;
   private IDistributedLockProvider lockProvider;
   private Configuration hibernateConfiguration;
   private EventListeners eventListeners;
   private boolean schemaUpdate;
   private StoreUpdateScriptsConfiguration updateScriptsConfiguration = new StoreUpdateScriptsConfiguration();
   private Object cacheRegionFactory;

   public LocalAnnotationEntityManagerFactory() {
      setJpaDialect(new HibernateJpaDialect());
      setJpaVendorAdapter(new HibernateJpaVendorAdapter());
      eventListeners = new EventListeners(null);
   }

   @Override
   @SuppressWarnings({ "unchecked" })
   protected EntityManagerFactory createNativeEntityManagerFactory() throws PersistenceException {
      try {
         configTimeRegionFactoryHolder.set(cacheRegionFactory);
         Properties properties = new Properties();
         properties.putAll(getJpaPropertyMap());
         Ejb3Configuration configuration = new Ejb3Configuration();
         configuration.setDataSource(getDataSource());
         configuration.addProperties(properties);
         for (Class aClass : annotatedClasses) {
            configuration.addAnnotatedClass(aClass);
         }
         for (Object location : configLocations) {
            configuration.configure((String) location);
         }
         for (Object resource : resources) {
            configuration.addResource((String) resource);
         }
         Map<String, Object> eListeners = eventListeners.listeners();
         for (Map.Entry<String, Object> entry : eListeners.entrySet()) {
            String listenerType = entry.getKey();
            Object listenerObject = entry.getValue();
            //
            Collection<Object> listeners = (Collection<Object>) listenerObject;
            org.hibernate.event.EventListeners listenerRegistry = configuration.getEventListeners();
            Object[] listenerArray = (Object[]) Array.newInstance(
                     listenerRegistry.getListenerClassFor(listenerType), listeners.size());
            listenerArray = listeners.toArray(listenerArray);
            configuration.setListeners(listenerType, listenerArray);
         }
         if (cacheRegionFactory != null) {
            configuration.setProperty(Environment.CACHE_REGION_FACTORY,
                     CacheFactoryProxy.class.getName());
         }
         configuration.setNamingStrategy(new ImprovedNamingStrategy());
         EntityManagerFactory emf = configuration.buildEntityManagerFactory();
         hibernateConfiguration = configuration.getHibernateConfiguration();
         eventListeners.initialize(hibernateConfiguration);
         storeSchemaCreateDeleteScripts(getConfiguration(), updateScriptsConfiguration);
         if (schemaUpdate) {
            updateDatabaseSchema();
         }
         postProcessEntityManagerFactory(emf, getPersistenceUnitInfo());
         return emf;
      } finally {
         configTimeRegionFactoryHolder.remove();
      }
   }

   @Override
   public void updateDatabaseSchema() {
      if (lockProvider != null) {
         log.info("using lockProvider %s for db schema update", lockProvider);
         Lock lock = lockProvider.lockFor(RTTP_DB_UPDATE_LOCK);
         try {
            if (lock.tryLock()) {
               try {
                  if (!lockProvider.isWorkDone()) {
                     performSchemaUpdate(getDataSource(), hibernateConfiguration,
                              updateScriptsConfiguration, databasePopulator);
                     lockProvider.markWorkDone();
                     log.info("db schema update done using lock %s", lock);
                  }
               } finally {
                  lock.unlock();
               }
            }
         } catch (DataAccessException e) {
            logger.error(e);
            throw e;
         }
      } else {
         performSchemaUpdate(getDataSource(), hibernateConfiguration, updateScriptsConfiguration,
                  databasePopulator);
      }
   }

   @Override
   public void destroy() throws HibernateException {
      try {
         eventListeners.cleanup();
      } finally {
         super.destroy();
      }
   }

   public SessionFactory getSessionFactory() {
      if (this.getObject() == null
               || ((HibernateEntityManagerFactory) this.getObject()).getSessionFactory() == null) {
         throw new IllegalStateException("SessionFactory not initialized yet");
      }
      return ((HibernateEntityManagerFactory) this.getObject()).getSessionFactory();
   }

   static ThreadLocal<Object> getConfigTimeRegionFactoryHolder() {
      return configTimeRegionFactoryHolder;
   }

   @Override
   public void setMappingResources(List<?> resources) {
      this.resources = resources;
   }

   @Override
   public void setAnnotatedClasses(List<Class> list) {
      this.annotatedClasses.addAll(list);
   }

   @Override
   @SuppressWarnings({ "unchecked" })
   public void setConfigLocations(List<?> configLocations) {
      this.configLocations.addAll(configLocations);
   }

   @Override
   public void setDatabasePopulator(DatabasePopulator databasePopulator) {
      this.databasePopulator = databasePopulator;
   }

   @Override
   public void setLockProvider(IDistributedLockProvider lockProvider) {
      this.lockProvider = lockProvider;
   }

   @Override
   public void setHibernateProperties(Properties properties) {
      setJpaProperties(hibernateProperties(properties));
   }

   @Override
   public Configuration getConfiguration() {
      return hibernateConfiguration;
   }

   @Override
   public void setSchemaUpdate(boolean schemaUpdate) {
      this.schemaUpdate = schemaUpdate;
   }

   @Override
   public void setUpdateScriptsConfiguration(StoreUpdateScriptsConfiguration cfg) {
      this.updateScriptsConfiguration = cfg;
   }

   @Override
   public StoreUpdateScriptsConfiguration getUpdateScriptsConfiguration() {
      return updateScriptsConfiguration;
   }

   @Override
   public void setCacheRegionFactory(Object cacheRegionFactory) {
      this.cacheRegionFactory = cacheRegionFactory;
   }

   @Override
   public void setEventListeners(Map<String, Object> listeners) {
      this.eventListeners = new EventListeners(listeners);
   }

   public List<Class> getAnnotatedClasses() {
      return annotatedClasses;
   }

   public List getConfigLocations() {
      return configLocations;
   }

   public List getResources() {
      return resources;
   }

   public DatabasePopulator getDatabasePopulator() {
      return databasePopulator;
   }

   public IDistributedLockProvider getLockProvider() {
      return lockProvider;
   }

   public Map<String, Object> getEventListeners() {
      return eventListeners.listeners();
   }

   public boolean isSchemaUpdate() {
      return schemaUpdate;
   }

   public Object getCacheRegionFactory() {
      return cacheRegionFactory;
   }
}
